Skip to content

fix(ci): resolve CI failures, skip warm-start ANALYZE, add init timing logs#200

Merged
tstapler merged 9 commits into
mainfrom
fix/ci-failures
Jul 3, 2026
Merged

fix(ci): resolve CI failures, skip warm-start ANALYZE, add init timing logs#200
tstapler merged 9 commits into
mainfrom
fix/ci-failures

Conversation

@tstapler

@tstapler tstapler commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • fix(ci): resolve pre-existing CI failures on main
  • fix(wasm): move ktoml out of commonMain to fix wasm-opt failure
  • fix(sections): add encodeSectionManifestToml expect/actual for SectionManifestWriter
  • perf(db): skip ANALYZE blocks/pages on warm starts when sqlite_stat1 already has stats — saves ~100ms per startup on Android by checking sqlite_stat1 before running ANALYZE instead of running it unconditionally every launch
  • debug(init): add elapsed-time log lines to switchGraph init sequence so the on-screen overlay shows exactly where init time is spent

Test plan

  • Business tests pass: bazel test //kmp:business_tests
  • JVM tests pass (pre-existing failures baseline: 54): bazel test //kmp:jvm_tests
  • On Android: warm-start overlay shows init[Xms]: createRepositorySet done where X is measurably lower than before
  • Fresh-install second startup still shows correct query plans (ANALYZE runs when sqlite_stat1 has no entry for blocks/pages)

🤖 Generated with Claude Code

tstapler and others added 5 commits July 1, 2026 22:04
- Detekt: fix MultipleEmitters in DeviceSetupWizard (wrap in Column),
  ComposableParamOrder in SectionBadge (modifier before onClick),
  ComplexCondition in SettingsDialog (extract to local val)
- Bazel Android: add ktoml dep missing from androidMain BUILD.bazel
- Wasm/JS: move js() calls to top-level functions (Kotlin/Wasm restriction),
  replace onLeft with Either.Left pattern match to avoid missing import
- SQLDelight: sync generated sources (idx_pages_section_id in create())

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ktoml 0.7.1 generates WASM bytecode that fails binaryen wasm-opt
validation during the production bundle optimization step. Move ktoml
to jvmCommonMain + iosMain and introduce an expect/actual split:
- jvmCommonMain/iosMain: actual uses ktoml for real TOML parsing
- wasmJsMain: actual returns null (SectionManifestParser falls back
  to empty SectionManifest — no file system in the browser anyway)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nManifestWriter

SectionManifestWriter.kt (commonMain) referenced sectionToml directly,
which was moved to jvmCommonMain in the ktoml WASM fix. Apply the same
expect/actual split: JVM+iOS actuals delegate to ktoml, WASM stub throws
(caught by the surrounding Either error handler).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tats

ANALYZE blocks + ANALYZE pages ran unconditionally every startup (~50ms
each on Android). Gate them on sqlite_stat1 having no entry for the table
— the only case that needs explicit ANALYZE is the second startup after a
fresh install (when the analyze_blocks migration ran on an empty table and
wrote no stats). Warm starts now run 2× cheap SELECT instead.
PRAGMA optimize continues to run unconditionally to handle stale stats
for all other tables.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Logs ms elapsed at each major step (preFlightJob, createRepositorySet,
UuidMigration, content migrations) so the overlay shows where init time
is actually spent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 2, 2026 16:07
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

JVM Load Benchmark (Desktop)

Synthetic in-memory benchmark measuring load performance for the desktop (JVM) app.
Comparing c68c0c26 (this PR) vs 929ffc81 (baseline)
Graph config: xlarge — 230 pages

Metric This PR Baseline Delta
Phase 1 TTI ↓ 0ms 0ms 0 (0%)
Phase 2 background ↓ 0ms 0ms 0 (0%)
Phase 3 index ↓ 0ms 1ms -1ms (-100%) ✅
Total ↓ 1ms 1ms 0 (0%)
Write p95 (baseline) ↓ 32ms 31ms +1ms (+3%) ⚠️
Write p95 (under load) ↓ n/a n/a
Jank factor ↓ n/a n/a
↓ lower is better
Flamegraphs (this PR) **Allocation** — object allocation pressure (JDBC/SQLite churn)

Alloc flamegraph not available

CPU — method-level hotspots by on-CPU time

CPU flamegraph not available

Top allocation hotspots (this PR) `36.4%` byte[]_[k] `7.6%` java.lang.String_[k] `7.1%` java.util.LinkedHashMap$Entry_[k] `6.8%` int[]_[k] `3.5%` java.lang.StringBuilder_[k]
Top CPU hotspots (this PR) `94.9%` /usr/lib/x86_64-linux-gnu/libc.so.6 `2%` /tmp/sqlite-3.51.3.0-3427415d-6fce-4c83-9f49-356c2bf6597f-libsqlitejdbc.so `0.7%` __libc_pwrite `0.5%` fsync `0.2%` pthread_cond_signal

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses CI failures and platform build issues (notably Kotlin/Wasm), introduces platform-specific TOML parsing/encoding for section manifests, and improves startup performance/observability by skipping redundant SQLite ANALYZE work on warm starts and adding init timing logs.

Changes:

  • Move section manifest TOML parsing/encoding behind expect/actual so ktoml is only used on JVM/iOS, with WASM stubs.
  • Reduce warm-start startup work by skipping ANALYZE blocks/pages when sqlite_stat1 already contains stats; add init elapsed-time logs in GraphManager.
  • Fix Kotlin/Wasm js() interop placement and apply minor UI/compose refactors.

Reviewed changes

Copilot reviewed 13 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
kmp/src/wasmJsMain/kotlin/dev/stapler/stelekit/sync/WasmSectionSyncService.kt Moves js() helpers to top-level for Kotlin/Wasm compatibility; replaces Arrow onLeft usage with explicit Either.Left check.
kmp/src/wasmJsMain/kotlin/dev/stapler/stelekit/sections/SectionManifestTomlDecoder.js.kt WASM actuals: decoding returns null (parser falls back), encoding throws unsupported op.
kmp/src/jvmCommonMain/kotlin/dev/stapler/stelekit/sections/SectionManifestTomlDecoder.jvm.kt JVM actuals using ktoml for decoding/encoding section manifest TOML.
kmp/src/iosMain/kotlin/dev/stapler/stelekit/sections/SectionManifestTomlDecoder.ios.kt iOS actuals using ktoml for decoding/encoding section manifest TOML.
kmp/src/generated/sqldelight/dev/stapler/stelekit/db/SteleDatabaseQueries.kt Generated SQLDelight changes (formatting/arg count expression).
kmp/src/generated/sqldelight/dev/stapler/stelekit/db/kmp/SteleDatabaseImpl.kt Generated schema creation adds idx_pages_section_id.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/onboarding/DeviceSetupWizard.kt Wraps mode selection/custom mode UI in Column containers.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/SettingsDialog.kt Refactors null-checks for Sections settings rendering; uses !! after consolidated condition.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/SectionBadge.kt Reorders parameters to put modifier before onClick.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/sections/SectionManifestWriter.kt Routes TOML encoding through encodeSectionManifestToml expect/actual.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/sections/SectionManifestParser.kt Routes TOML decoding through decodeSectionManifestToml expect/actual with WASM fallback to empty manifest.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/db/MigrationRunner.kt Skips ANALYZE when stats already exist; adds hasStats helper.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/db/GraphManager.kt Adds elapsed-time log lines across switchGraph init sequence.
kmp/src/androidMain/kotlin/BUILD.bazel Adds ktoml dependency for Android Bazel build.
kmp/build.gradle.kts Moves ktoml dependency out of commonMain into jvmCommonMain and iosMain.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread kmp/src/commonMain/kotlin/dev/stapler/stelekit/db/MigrationRunner.kt Outdated
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Android Load Benchmark

Instrumented benchmark on an API 30 x86_64 emulator — 500-page synthetic graph.

Comparing c68c0c26 (this PR) vs 929ffc81 (baseline)
Device: API 30 x86_64 emulator — 530 pages loaded

Graph Load

Metric This PR Baseline Delta
Phase 1 TTI ↓ 32ms 43ms -11ms (-26%) ✅
Phase 3 index ↓ 3627ms 3730ms -103ms (-3%) ✅

Interactive Write Latency (during Phase 3)

Metric This PR Baseline Delta
Write p95 (baseline) ↓ 7ms 7ms 0 (0%)
Write p95 (during phase 3) ↓ 11ms 13ms -2ms (-15%) ✅
Jank factor ↓ 1.57x 1.86x -0.29x (-16%) ✅
Concurrent writes ↑ 18 18 0 (0%)

SAF I/O Overhead (ContentProvider vs direct File read)

Measures Binder IPC cost added by ContentResolver per readFile() call.
Real SAF via ExternalStorageProvider will be higher on device; this is a lower bound.

Metric This PR Baseline Delta
Direct read / file ↓ 0.0ms 0.0ms 0 (0%)
Provider read / file ↓ 0.2ms 0.2ms 0 (0%)
IPC overhead ratio ↓ 6x 6x 0 (0%)
↓ lower is better · ↑ higher is better

tstapler and others added 4 commits July 2, 2026 17:15
…ce cold-start shadow timeout

- RepositoryFactory: open telemetry DB in a parallel coroutine alongside main DB init
  (write + read driver opens). No data dependency between them; on a real device each
  open is 50–250ms, so this reduces createRepositorySet wall-time by that amount.
  runBlocking join is bounded — telemetry typically completes before the main DB lazy.

- GraphManager: emit a "db.init" span (startMs → after content migrations) using the
  repo set's spanEmitter. Makes driver-open + migration latency visible in the Spans
  tab and eligible for an SLO threshold; previously this entire path was uninstrumented.

- GraphLoader: reduce SHADOW_STARTUP_TIMEOUT_MS from 2000ms → 500ms. Failure is
  non-fatal and SAF cursor queries complete well under 500ms; the old 2s ceiling was
  the worst-case TTI penalty on cold starts with a slow SAF provider.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…epositorySet

- MigrationRunner.hasStats: add catch(e: CancellationException) { throw e } to
  match the structured-concurrency contract used everywhere else in the file;
  without it, coroutine cancellation on WASM (single-threaded dispatcher) would
  be swallowed and trigger spurious ANALYZE work on a cancelled scope
- RepositoryFactory.createRepositorySet: promote to suspend fun and replace
  runBlocking { telemetryDeferred.await() } with direct await(); removes the
  hidden blocking call that could deadlock on single-threaded dispatchers (WASM)
  and violates the method's implied non-blocking contract; all 15+ call sites
  are already inside runBlocking/runTest so no callers need changing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Address two copilot review comments on MigrationRunner:
- Update the comment block above the conditional ANALYZE calls to correctly
  describe the new warm-start skip behavior; removes the old "unconditionally"
  wording that no longer applied
- Parameterize the hasStats SQL query (? + bindString) instead of string
  interpolation; matches the parameterized convention used elsewhere in the file
  and prevents accidental injection if the private helper is ever reused

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n blocks

Two catch(_: Exception) blocks in RepositoryFactory.createRepositorySet were
swallowing CancellationException. Detekt flagged both. Add the standard guard
to match the pattern used throughout the codebase.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@tstapler tstapler merged commit 7f9ecbd into main Jul 3, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants